using System.Runtime.InteropServices;
using static ServiceLib.Handler.SysProxy.ProxySettingWindows.InternetConnectionOption;

namespace ServiceLib.Handler.SysProxy;

public class ProxySettingWindows
{
    private const string _regPath = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings";

    private static bool SetProxyFallback(string? strProxy, string? exceptions, int type)
    {
        if (type == 1)
        {
            WindowsUtils.RegWriteValue(_regPath, "ProxyEnable", 0);
            WindowsUtils.RegWriteValue(_regPath, "ProxyServer", string.Empty);
            WindowsUtils.RegWriteValue(_regPath, "ProxyOverride", string.Empty);
            WindowsUtils.RegWriteValue(_regPath, "AutoConfigURL", string.Empty);
        }
        if (type == 2)
        {
            WindowsUtils.RegWriteValue(_regPath, "ProxyEnable", 1);
            WindowsUtils.RegWriteValue(_regPath, "ProxyServer", strProxy ?? string.Empty);
            WindowsUtils.RegWriteValue(_regPath, "ProxyOverride", exceptions ?? string.Empty);
            WindowsUtils.RegWriteValue(_regPath, "AutoConfigURL", string.Empty);
        }
        else if (type == 4)
        {
            WindowsUtils.RegWriteValue(_regPath, "ProxyEnable", 0);
            WindowsUtils.RegWriteValue(_regPath, "ProxyServer", string.Empty);
            WindowsUtils.RegWriteValue(_regPath, "ProxyOverride", string.Empty);
            WindowsUtils.RegWriteValue(_regPath, "AutoConfigURL", strProxy ?? string.Empty);
        }
        return true;
    }

    /// <summary>
    // set to use no proxy
    /// </summary>
    /// <exception cref="ApplicationException">Error message with win32 error code</exception>
    public static bool UnsetProxy()
    {
        return SetProxy(null, null, 1);
    }

    /// <summary>
    /// Set system proxy settings
    /// </summary>
    /// <param name="strProxy"> proxy address</param>
    /// <param name="exceptions">exception addresses that do not use proxy</param>
    /// <param name="type">type of proxy defined in PerConnFlags
    ///     PROXY_TYPE_DIRECT           = 0x00000001, // direct connection (no proxy)
    ///     PROXY_TYPE_PROXY            = 0x00000002, // via named proxy
    ///     PROXY_TYPE_AUTO_PROXY_URL   = 0x00000004, // autoproxy script URL
    ///     PROXY_TYPE_AUTO_DETECT      = 0x00000008  // use autoproxy detection
    /// </param>
    /// <exception cref="ApplicationException">Error message with win32 error code</exception>
    /// <returns>true: one of connection is successfully updated proxy settings</returns>
    public static bool SetProxy(string? strProxy, string? exceptions, int type)
    {
        try
        {
            // set proxy for LAN
            var result = SetConnectionProxy(null, strProxy, exceptions, type);
            // set proxy for dial up connections
            var connections = EnumerateRasEntries();
            foreach (var connection in connections)
            {
                result |= SetConnectionProxy(connection, strProxy, exceptions, type);
            }
            return result;
        }
        catch
        {
            _ = SetProxyFallback(strProxy, exceptions, type);
            return false;
        }
    }

    private static bool SetConnectionProxy(string? connectionName, string? strProxy, string? exceptions, int type)
    {
        var list = new InternetPerConnOptionList();

        var optionCount = 1;
        if (type == 1) // No proxy
        {
            optionCount = 1;
        }
        else if (type is 2 or 4) // named proxy or autoproxy script URL
        {
            optionCount = exceptions.IsNullOrEmpty() ? 2 : 3;
        }

        var m_Int = (int)PerConnFlags.PROXY_TYPE_DIRECT;
        var m_Option = PerConnOption.INTERNET_PER_CONN_FLAGS;
        if (type == 2) // named proxy
        {
            m_Int = (int)(PerConnFlags.PROXY_TYPE_DIRECT | PerConnFlags.PROXY_TYPE_PROXY);
            m_Option = PerConnOption.INTERNET_PER_CONN_PROXY_SERVER;
        }
        else if (type == 4) // autoproxy script url
        {
            m_Int = (int)(PerConnFlags.PROXY_TYPE_DIRECT | PerConnFlags.PROXY_TYPE_AUTO_PROXY_URL);
            m_Option = PerConnOption.INTERNET_PER_CONN_AUTOCONFIG_URL;
        }

        var options = new InternetConnectionOption[optionCount];
        // USE a proxy server ...
        options[0].m_Option = PerConnOption.INTERNET_PER_CONN_FLAGS;
        options[0].m_Value.m_Int = m_Int;
        // use THIS proxy server
        if (optionCount > 1)
        {
            options[1].m_Option = m_Option;
            options[1].m_Value.m_StringPtr = Marshal.StringToHGlobalAuto(strProxy); // !! remember to deallocate memory 1
                                                                                    // except for these addresses ...
            if (optionCount > 2)
            {
                options[2].m_Option = PerConnOption.INTERNET_PER_CONN_PROXY_BYPASS;
                options[2].m_Value.m_StringPtr = Marshal.StringToHGlobalAuto(exceptions); // !! remember to deallocate memory 2
            }
        }

        // default stuff
        list.dwSize = Marshal.SizeOf(list);
        if (connectionName != null)
        {
            list.szConnection = Marshal.StringToHGlobalAuto(connectionName); // !! remember to deallocate memory 3
        }
        else
        {
            list.szConnection = nint.Zero;
        }
        list.dwOptionCount = options.Length;
        list.dwOptionError = 0;

        var optSize = Marshal.SizeOf(typeof(InternetConnectionOption));
        // make a pointer out of all that ...
        var optionsPtr = Marshal.AllocCoTaskMem(optSize * options.Length); // !! remember to deallocate memory 4
                                                                           // copy the array over into that spot in memory ...
        for (var i = 0; i < options.Length; ++i)
        {
            if (Environment.Is64BitOperatingSystem)
            {
                var opt = new nint(optionsPtr.ToInt64() + (i * optSize));
                Marshal.StructureToPtr(options[i], opt, false);
            }
            else
            {
                var opt = new nint(optionsPtr.ToInt32() + (i * optSize));
                Marshal.StructureToPtr(options[i], opt, false);
            }
        }

        list.options = optionsPtr;

        // and then make a pointer out of the whole list
        var ipcoListPtr = Marshal.AllocCoTaskMem(list.dwSize); // !! remember to deallocate memory 5
        Marshal.StructureToPtr(list, ipcoListPtr, false);

        // and finally, call the API method!
        var isSuccess = NativeMethods.InternetSetOption(nint.Zero,
           InternetOption.INTERNET_OPTION_PER_CONNECTION_OPTION,
           ipcoListPtr, list.dwSize);
        var returnvalue = 0; // ERROR_SUCCESS
        if (!isSuccess)
        {  // get the error codes, they might be helpful
            returnvalue = Marshal.GetLastPInvokeError();
        }
        else
        {
            // Notify the system that the registry settings have been changed and cause them to be refreshed
            _ = NativeMethods.InternetSetOption(nint.Zero, InternetOption.INTERNET_OPTION_SETTINGS_CHANGED, nint.Zero, 0);
            _ = NativeMethods.InternetSetOption(nint.Zero, InternetOption.INTERNET_OPTION_REFRESH, nint.Zero, 0);
        }

        // FREE the data ASAP
        if (list.szConnection != nint.Zero)
        {
            Marshal.FreeHGlobal(list.szConnection); // release mem 3
        }
        if (optionCount > 1)
        {
            Marshal.FreeHGlobal(options[1].m_Value.m_StringPtr); // release mem 1
            if (optionCount > 2)
            {
                Marshal.FreeHGlobal(options[2].m_Value.m_StringPtr); // release mem 2
            }
        }
        Marshal.FreeCoTaskMem(optionsPtr); // release mem 4
        Marshal.FreeCoTaskMem(ipcoListPtr); // release mem 5
        if (returnvalue != 0)
        {
            // throw the error codes, they might be helpful
            throw new ApplicationException($"Set Internet Proxy failed with error code: {Marshal.GetLastWin32Error()}");
        }

        return true;
    }

    /// <summary>
    /// Retrieve list of connections including LAN and WAN to support PPPoE connection
    /// </summary>
    /// <returns>A list of RAS connection names. May be empty list if no dial up connection.</returns>
    /// <exception cref="ApplicationException">Error message with win32 error code</exception>
    private static IEnumerable<string> EnumerateRasEntries()
    {
        var entries = 0;
        // attempt to query with 1 entry buffer
        var rasEntryNames = new RASENTRYNAME[1];
        var bufferSize = Marshal.SizeOf(typeof(RASENTRYNAME));
        rasEntryNames[0].dwSize = Marshal.SizeOf(typeof(RASENTRYNAME));

        var result = NativeMethods.RasEnumEntries(null, null, rasEntryNames, ref bufferSize, ref entries);
        // increase buffer if the buffer is not large enough
        if (result == (uint)ErrorCode.ERROR_BUFFER_TOO_SMALL)
        {
            rasEntryNames = new RASENTRYNAME[bufferSize / Marshal.SizeOf(typeof(RASENTRYNAME))];
            for (var i = 0; i < rasEntryNames.Length; i++)
            {
                rasEntryNames[i].dwSize = Marshal.SizeOf(typeof(RASENTRYNAME));
            }

            result = NativeMethods.RasEnumEntries(null, null, rasEntryNames, ref bufferSize, ref entries);
        }
        if (result == 0)
        {
            var entryNames = new List<string>();
            for (var i = 0; i < entries; i++)
            {
                entryNames.Add(rasEntryNames[i].szEntryName);
            }

            return entryNames;
        }
        throw new ApplicationException($"RasEnumEntries failed with error code: {result}");
    }

    #region WinInet structures

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    public struct InternetPerConnOptionList
    {
        public int dwSize;               // size of the INTERNET_PER_CONN_OPTION_LIST struct
        public nint szConnection;         // connection name to set/query options
        public int dwOptionCount;        // number of options to set/query
        public int dwOptionError;           // on error, which option failed

        //[MarshalAs(UnmanagedType.)]
        public nint options;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    public struct InternetConnectionOption
    {
        private static readonly int Size;
        public PerConnOption m_Option;
        public InternetConnectionOptionValue m_Value;

        static InternetConnectionOption()
        {
            Size = Marshal.SizeOf(typeof(InternetConnectionOption));
        }

        // Nested Types
        [StructLayout(LayoutKind.Explicit)]
        public struct InternetConnectionOptionValue
        {
            // Fields
            [FieldOffset(0)]
            public System.Runtime.InteropServices.ComTypes.FILETIME m_FileTime;

            [FieldOffset(0)]
            public int m_Int;

            [FieldOffset(0)]
            public nint m_StringPtr;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct RASENTRYNAME
        {
            public int dwSize;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = RAS_MaxEntryName + 1)]
            public string szEntryName;

            public int dwFlags;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH + 1)]
            public string szPhonebookPath;
        }

        // Constants
        public const int RAS_MaxEntryName = 256;

        public const int MAX_PATH = 260; // Standard MAX_PATH value in Windows
    }

    #endregion WinInet structures

    #region WinInet enums

    //
    // options manifests for Internet{Query|Set}Option
    //
    public enum InternetOption : uint
    {
        INTERNET_OPTION_PER_CONNECTION_OPTION = 75,
        INTERNET_OPTION_REFRESH = 37,
        INTERNET_OPTION_SETTINGS_CHANGED = 39
    }

    //
    // Options used in INTERNET_PER_CONN_OPTON struct
    //
    public enum PerConnOption
    {
        INTERNET_PER_CONN_FLAGS = 1, // Sets or retrieves the connection type. The Value member will contain one or more of the values from PerConnFlags
        INTERNET_PER_CONN_PROXY_SERVER = 2, // Sets or retrieves a string containing the proxy servers.
        INTERNET_PER_CONN_PROXY_BYPASS = 3, // Sets or retrieves a string containing the URLs that do not use the proxy server.
        INTERNET_PER_CONN_AUTOCONFIG_URL = 4//, // Sets or retrieves a string containing the URL to the automatic configuration script.
    }

    //
    // PER_CONN_FLAGS
    //
    [Flags]
    public enum PerConnFlags
    {
        PROXY_TYPE_DIRECT = 0x00000001,  // direct to net
        PROXY_TYPE_PROXY = 0x00000002,  // via named proxy
        PROXY_TYPE_AUTO_PROXY_URL = 0x00000004,  // autoproxy URL
        PROXY_TYPE_AUTO_DETECT = 0x00000008   // use autoproxy detection
    }

    public enum ErrorCode : uint
    {
        ERROR_BUFFER_TOO_SMALL = 603,
        ERROR_INVALID_SIZE = 632
    }

    #endregion WinInet enums

    internal static class NativeMethods
    {
        [DllImport("WinInet.dll", SetLastError = true, CharSet = CharSet.Auto)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool InternetSetOption(nint hInternet, InternetOption dwOption, nint lpBuffer, int dwBufferLength);

        [DllImport("Rasapi32.dll", CharSet = CharSet.Auto)]
        public static extern uint RasEnumEntries(
            string? reserved,          // Reserved, must be null
            string? lpszPhonebook,     // Pointer to full path and filename of phone-book file. If this parameter is NULL, the entries are enumerated from all the remote access phone-book files
            [In, Out] RASENTRYNAME[]? lprasentryname, // Buffer to receive RAS entry names
            ref int lpcb,             // Size of the buffer
            ref int lpcEntries        // Number of entries written to the buffer
        );
    }
}
